import {Cask} from 'cascadium';
import Accrual from '../Accrual';
import Identity from '../Identity';
import {propFunc} from '../prop-func';

export const geneFunc = (propSpec='curr.code', delim='') => (rec) => {
  const propVal = propFunc(rec, propSpec);
  if (delim === '') {
    return propVal.length;
  } else {
    return propVal.split(delim).length;
  }
}

function arrayPrefix(array, patt) { 
  for (let i = 0; i < patt.length; i++) if (array[i] !== patt[i]) {
    return false;
  }
  return true;
}

export const seekFunc = (propSpec='curr.code', delim='', postMatch=() => {}) => (prev, succ) => {
  const prevVal = propFunc(prev, propSpec);
  const succVal = propFunc(succ, propSpec);
  const isMatching = delim === ''
    ? succVal.startsWith(prevVal)
    : arrayPrefix(succVal.split(delim), prevVal.split(delim))

  if (isMatching) postMatch(prev, succ);

  return isMatching
}

class Entry {

  constructor(entry={}, {emptyAccrual=false, emptyIdentity=false}={}) {

    const {curr, code, name, itemCode, itemName, dest, destCode, destName, accrual, cr, dr, attr, ...restAttr} = entry;
    
    this.curr = emptyIdentity
    ? new Identity({code:'NONE', name: 'NONE'})
    : curr ?? new Identity({code, name, itemCode, itemName});
    
    this.dest = dest ?? new Identity({code: destCode, name: destName});

    this.accrual = emptyAccrual
    ? new Accrual({cr: 0, dr: 0})
    : accrual?.copy() ?? new Accrual({cr, dr});

    this.attr = JSON.parse(JSON.stringify(attr ?? {type: 'RAW', ...restAttr}));

    this.succ = new Cask();

    this.pred;
    this.path = this.curr.name;
  }

  copy(opts={}) {
    const Cons = this.constructor;
    return new Cons(this, opts);
  }
  
  move(targ, {withProto=false, pushSide='HEAD', withPath=true}={}) {
    const self = withProto ? this.pushProto() : this;
    
    if (pushSide === 'HEAD') {
      targ.succ.unshift(self);
    } else {
      targ.succ.push(self);
    }
    self.pred = targ;
    
    if (withPath) {
      self.path = `${targ.path}/${self.path}`;
      self.trav(subEntry => {
        subEntry.path = `${targ.path}/${subEntry.path}`
      })
    }
  }

  trav(action=e=>e, dir='PRE', skip){

    if (skip) return;

    if (!['POST', 'PRE'].includes(dir)){
      throw Error('trav: direction must be either POST or PRE');
    }

    for (let index = 0; index < this.succ.length; index ++) {
      const entry = this.succ[index];
      let skipNext = false;
      if (dir === 'PRE') {
        skipNext = action(entry, index);
      };
      entry.trav(action, dir, skipNext);
      (dir === 'POST') && action(entry, index);
    }
  }

  travMap(mapFunc) {
    const res = new Cask();

    this.trav(({succ}) => {
      for (let i = 0; i < succ.length; i++) {
        res.push(mapFunc(succ[i]));
      }
    })

    return res;
  }

  all(propSpec, val) {
    const res = new Cask();
    this.trav(({succ}) => {
      const resAll = succ.all(propSpec, val);
      while(resAll.length > 0) {
        res.push(resAll.pop());
      }
    });
    return res;
  }

  nestFrom(entries, nestProp='curr.code', delim="") {

    const move = (prev, succ) =>
     succ.move(prev, {withProto: false, withPath: true, pushSide:'TAIL'});

    const entriesCopied = new Cask(...entries);
    const entriesNested = entriesCopied.nest(geneFunc(nestProp, delim), seekFunc(nestProp, delim, move));

    for (let i = 0; i < entriesNested.length; i++) {
      entriesNested[i].move(this, {withProto: false, withPath: false, pushSide:'TAIL'});
    }

    return this;
  }

  unnestTo(unnestFunc) {
    const func = unnestFunc ?? (({succ}) => succ);
    return this.succ.unnest(func);
  }

  findPath(pathSpec) {

    let res;

    this.trav(rec => {
      if (rec.path === pathSpec) res ??= rec;
    })

    return res;
  }

  backtrav() {
    const trace = new Cask();
    for (let curr = this; curr; curr = curr.pred) trace.push(curr);
    return trace;
  }

  pushProto() {
    const self = Object.create(this);
    const SuccCons = this.succ.constructor;
    self.pred = undefined;
    self.succ = new SuccCons();
    return self;
  }

  popProto() {
    return this.__proto__;
  }
}

export default Entry;